﻿#region Header
// ------------------------ Licence / Copyright ------------------------
// 
// ScrumTable for Scrum Meeting Support
// Copyright © HSR - Hochschule für Technik Rapperswil
// All Rights Reserved
// 
// Author:
//  Michael Gfeller Silvan Gehrig Patrick Boos
// 
// ---------------------------------------------------------------------
#endregion

#region Usings

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.TeamFoundation.WorkItemTracking.Client;
using ScrumTable.Common;
using ScrumTable.Common.Collections;
using ScrumTable.Common.Data;
using ScrumTable.Common.Logging;
using ScrumTable.Common.Net;
using ScrumTable.Common.Security;
using ScrumTable.Common.Threading;
using ScrumTable.DL.Data.Generic.Cache;
using ScrumTable.DL.Data.Schema;
using ScrumTable.DL.Data.Schema.ScrumTableV1;
using ScrumTable.DL.Data.TFS.Bulk.Properties;
using ScrumTable.DL.Data.TFS.Services.TeamFoundation;
using ScrumTable.DL.Data.TFS.Services.TeamFoundation.Utils;

#endregion

namespace ScrumTable.DL.Data.TFS.Bulk.Services
{
    /// <summary>
    /// This class is used to initiate the connection and load all needed
    /// information to access data on the project level.
    /// </summary>
    internal partial class ConnectionCacheLoader
    {
        #region Declarations
        //--------------------------------------------------------------------
        // Declarations
        //--------------------------------------------------------------------

        private const string StrictWorkflowSchemaAttribute = "IsStrictWorkflow";

        private readonly ConnectionInformationManager _connectionCtx;
        private readonly Uri _backEndSystemUri;
        private readonly ScrumTableV1Schema _schema;
        private readonly bool _loadPortalData;
        private readonly bool _loadReportingData;

        #endregion

        #region Properties
        //--------------------------------------------------------------------
        // Properties
        //--------------------------------------------------------------------

        /// <summary>
        /// Gets the sharepoint server uris, which are associated to
        /// a corresponding project (name).
        /// </summary>
        private IDictionary<string, ServiceRegistration> ProjectPortalRegistrations { get; set; }

        /// <summary>
        /// Gets the reporting server uris, which are associated to
        /// a corresponding project (name).
        /// </summary>
        private IDictionary<string, ServiceRegistration> RsProjectRegistrations { get; set; }

        private IList<SchemaBase> WorkItemTypeSchemata
        {
            get
            {
                return new SchemaBase[]
                            {
                                _schema.TaskSchema,
                                _schema.UserStorySchema,
                                _schema.BugSchema
                            };
            }
        }

        protected TfsMemberLoader MemberLoader { get; set; }
        private IList<int> AuthorizedProjectIds { get; set; }

        #endregion

        #region Constructors / Destructor
        //--------------------------------------------------------------------
        // Constructors / Destructor
        //--------------------------------------------------------------------

        /// <summary>
        /// Creates a new ConnectionCacheLoader instance, no public constructor
        /// needed.
        /// </summary>
        internal ConnectionCacheLoader(
            ConnectionInformationManager connectionCtx,
            Uri backEndSystemUri,
            ScrumTableV1Schema schema,
            bool loadPortalData,
            bool loadReportingData)
        {
            PreCondition.AssertNotNull(connectionCtx, "connectionCtx");
            PreCondition.AssertNotNull(backEndSystemUri, "backEndSystemUri");
            PreCondition.AssertNotNull(schema, "schema");

            _connectionCtx = connectionCtx;
            _backEndSystemUri = backEndSystemUri;
            _schema = schema;
            _loadPortalData = loadPortalData;
            _loadReportingData = loadReportingData;
        }

        #endregion

        #region Methods
        //--------------------------------------------------------------------
        // Methods
        //--------------------------------------------------------------------

        /// <summary>
        /// Loads the data which is required to connect to the back end tfs
        /// system.
        /// </summary>
        public void LoadData(AsyncCallback callback)
        {
            new AsyncCall<AsyncCallback>(LoadDataAsync).Execute(callback);
        }

        private void LoadDataAsync(AsyncCallback callback)
        {
            try
            {
                PrepareConnectionInfoAndProjects();

                using (TfsServerWrapper tfsInstance = new TfsConnector(_connectionCtx.Selected, _backEndSystemUri).Connect())
                {
                    // load data
                    InitRegistrationEntries(tfsInstance);
                    InitMemberLoader(tfsInstance);
                    callback(new DataResult<CacheContext>(LoadProjects(tfsInstance)));
                }
            }
            catch (Exception e)
            {
                callback(new DataResult<CacheContext>(null, e));
            }
        }

        /// <summary>
        /// Prepares the credentials for use with the credential manager. So
        /// this method will initialze the BackEndId properites of the ConnectionInformation
        /// instances.
        /// </summary>
        private void PrepareConnectionInfoAndProjects()
        {
            AuthorizedProjectIds = new List<int>();
            bool isFirst = true;

            foreach (ConnectionInformation connectionInfo in _connectionCtx)
            {
                // establish connection & check if the given credentials are valid
                using (TfsServerWrapper teamFoundationServer = new TfsServerWrapper(connectionInfo, _backEndSystemUri))
                {
                    teamFoundationServer.EnsureAuthenticated();
                    connectionInfo.BackEndId = teamFoundationServer.AuthorizedIdentity.GetUniqueId();

                    if (isFirst)
                        teamFoundationServer.WorkItems.Projects.ForEachProjectWithAuthority(p => AuthorizedProjectIds.Add(p.Id));
                    else
                        CheckProjectRightsForMember(teamFoundationServer);
                }
                isFirst = false;
            }
        }

        private void CheckProjectRightsForMember(TfsServerWrapper teamFoundationServer)
        {
            for (int i = AuthorizedProjectIds.Count - 1; i > -1; --i)
            {
                if (!HasProjectRightForMember(teamFoundationServer, i))
                    AuthorizedProjectIds.RemoveAt(i);
            }
        }

        private bool HasProjectRightForMember(TfsServerWrapper teamFoundationServer, int projectIndex)
        {
            Project p = (from project in teamFoundationServer.WorkItems.Projects.GetProjectsWithAuthority()
                         where project.Id == AuthorizedProjectIds[projectIndex]
                         select project).FirstOrDefault();
            return (p != null);
        }

        private void InitRegistrationEntries(TfsServerWrapper tfsInstance)
        {
            TfsRegistrationLoader loader = new TfsRegistrationLoader(tfsInstance.Registrations);

            if (_loadPortalData) { ProjectPortalRegistrations = loader.GetSharepointServerUris(); }
            else { ProjectPortalRegistrations = new Dictionary<string, ServiceRegistration>(0); }

            if (_loadReportingData) { RsProjectRegistrations = loader.GetReportingServerUris(); }
            else { RsProjectRegistrations = new Dictionary<string, ServiceRegistration>(0); }
        }

        private void InitMemberLoader(TfsServerWrapper tfsInstance)
        {
            MemberLoader = new TfsMemberLoader(tfsInstance.IdentityManagement);
        }


        private CacheContext LoadProjects(TfsServerWrapper tfsInstance)
        {
            CacheContext dataContext = new CacheContext(_schema);
            IList<CacheProject> projects = new List<CacheProject>(tfsInstance.WorkItems.Projects.Count);

            foreach (Project project in tfsInstance.WorkItems.Projects)
            {
                // preconditions:
                //  1. we need to have the appropriate project access permissions
                //    (AND)
                //  2. all required work item links must be available
                if (!AuthorizedProjectIds.Contains(project.Id) || !TfsLinkHandler.LinksAvaiable(project.Store))
                {
                    continue;
                }

                IList<CacheWorkItemType> types = LoadWorkItemTypes(dataContext, project);

                if (types == null)
                {
                    continue;
                }

                IList<CacheMember> members = null;
                IList<CacheTeam> teams = null;
                LoadMemberList(ref teams, ref members, tfsInstance, dataContext, project);

                projects.Add(dataContext.ElementFactory.CreateProject(
                    project.Id,
                    project.Name,
                    GetWssService(project),
                    GetRsService(project),
                    teams,
                    members,
                    types));
            }

            dataContext.Import(projects);
            return dataContext;
        }

        private ServiceRegistration GetWssService(Project project)
        {
            return ProjectPortalRegistrations.ContainsKey(project.Name) ? ProjectPortalRegistrations[project.Name] : ServiceRegistration.Empty;
        }

        private ServiceRegistration GetRsService(Project project)
        {
            return RsProjectRegistrations.ContainsKey(project.Name) ? RsProjectRegistrations[project.Name] : ServiceRegistration.Empty;
        }

        private ICollection<string> GetAssignedToValues(Project project)
        {
            return project.WorkItemTypes[_schema.UserStorySchema.SchemaMappedName].FieldDefinitions[_schema.UserStorySchema.MemberId.SchemaMappedName].AllowedValues.Convert(value => value.ToString());
        }

        private IList<CacheWorkItemType> LoadWorkItemTypes(CacheContext dataContext, Project project)
        {
            int wiTypeCount = WorkItemTypeSchemata.Count;
            IList<WorkItemTypeInfo> tfsTypes = RetrieveTfsWiTypes(project);
            IList<CacheWorkItemType> types = new List<CacheWorkItemType>(wiTypeCount);

            if (tfsTypes.Count == wiTypeCount)
            {
                foreach (WorkItemTypeInfo workTypeInfo in tfsTypes)
                {
                    CacheWorkItemType loadedType = LoadWorkItemType(dataContext, workTypeInfo);

                    if (loadedType != null)
                    {
                        types.Add(loadedType);
                    }
                }
            }

            if (types.Count != wiTypeCount)
            {
                return null;
            }
            return types;
        }

        private IList<WorkItemTypeInfo> RetrieveTfsWiTypes(Project project)
        {
            IList<WorkItemTypeInfo> tfsTypes = new List<WorkItemTypeInfo>();

            foreach (SchemaBase workItemTypeSchema in WorkItemTypeSchemata)
            {
                if (!string.IsNullOrEmpty(workItemTypeSchema.SchemaMappedName)
                    && project.WorkItemTypes.Contains(workItemTypeSchema.SchemaMappedName))
                {
                    tfsTypes.Add(
                        new WorkItemTypeInfo
                            {
                                TypeSchema = workItemTypeSchema,
                                WiType = project.WorkItemTypes[workItemTypeSchema.SchemaMappedName]
                            } );
                }
            }
            return tfsTypes;
        }

        private CacheWorkItemType LoadWorkItemType(CacheContext dataContext, WorkItemTypeInfo wiTypeInfo)
        {
            if (wiTypeInfo.TypeSchema[CacheWorkItemType.StateFieldName] == null) { return null; }
            if (!wiTypeInfo.WiType.FieldDefinitions.Contains(wiTypeInfo.TypeSchema[CacheWorkItemType.StateFieldName].SchemaMappedName)) { return null; }

            TfsWiTypeReader typeReader = new TfsWiTypeReader(wiTypeInfo.WiType);
            CacheWorkflow workflow = dataContext.ElementFactory.CreateWorkflow(
                LoadTransitions(dataContext, typeReader),
                wiTypeInfo.TypeSchema[CacheWorkItemType.StateFieldName],
                wiTypeInfo.TypeSchema.TryGetProperty(StrictWorkflowSchemaAttribute, true),
                wiTypeInfo.WiType.FieldDefinitions[wiTypeInfo.TypeSchema[CacheWorkItemType.StateFieldName].SchemaMappedName].AllowedValues.Convert<string>());

            IList<CacheFieldDefinition> fieldDefs = LoadFieldDefs(
                dataContext, wiTypeInfo.TypeSchema, wiTypeInfo.WiType.FieldDefinitions,
                typeReader, workflow);

            if (fieldDefs != null)
            {
                CacheWorkItemType workItemType = dataContext.ElementFactory.CreateWorkItemType(
                        wiTypeInfo.WiType.Name, wiTypeInfo.TypeSchema, fieldDefs,
                        workflow);

                if (workItemType.IsValid)
                {
                    return workItemType;
                }
            }
            return null;
        }

        private IEnumerable<CacheTransition> LoadTransitions(CacheContext dataContext, TfsWiTypeReader typeReader)
        {
            return typeReader.GetWorkflowTransitions().Convert(
                trans => dataContext.ElementFactory.CreateTransition(trans.From, trans.To));
        }

        private IList<CacheFieldDefinition> LoadFieldDefs(CacheContext dataContext, SchemaBase typeSchema, FieldDefinitionCollection defs, TfsWiTypeReader typeReader, CacheWorkflow workflow)
        {
            IDictionary<SchemaBase, FieldDefinition> fieldDefinitionMapByRef = new Dictionary<SchemaBase, FieldDefinition>();
            IDictionary<SchemaBase, FieldDefinition> fieldDefinitionMapByName = new Dictionary<SchemaBase, FieldDefinition>();
            
            // first try: lets map the fields by their reference (mapped) name
            LoadFieldDefMapBy(fieldDefinitionMapByRef, defs, def => (typeSchema.ContainsSchema(def.ReferenceName, true) ? typeSchema[def.ReferenceName, true] : null));

            // second try: lets map the fields by their name
            LoadFieldDefMapBy(fieldDefinitionMapByName, defs, def => (typeSchema.ContainsSchema(def.Name) ? typeSchema[def.Name] : null));

            // validate field definition map integrity
            IDictionary<SchemaBase, FieldDefinition> validatedFieldDefinitionMap = ValidateFieldDefMapIntegrity(
                fieldDefinitionMapByRef,
                fieldDefinitionMapByName,
                typeSchema);

            // initialize field definitions from TFS
            return validatedFieldDefinitionMap.Convert(mappedFieldPair => LoadFieldDef(dataContext, mappedFieldPair.Value, mappedFieldPair.Key, typeReader, workflow));
        }

        private IDictionary<SchemaBase, FieldDefinition> ValidateFieldDefMapIntegrity(
            IDictionary<SchemaBase, FieldDefinition> fieldDefinitionMapByRef,
            IDictionary<SchemaBase, FieldDefinition> fieldDefinitionMapByName,
            SchemaBase typeSchema)
        {
            IDictionary<SchemaBase, FieldDefinition> resultMap = new Dictionary<SchemaBase, FieldDefinition>();
            fieldDefinitionMapByName.ForEach(fieldNamePair => resultMap[fieldNamePair.Key] = fieldNamePair.Value);
            fieldDefinitionMapByRef.ForEach(fieldRefPair => resultMap[fieldRefPair.Key] = fieldRefPair.Value);

            foreach (SchemaBase fieldSchema in typeSchema.Schemata)
            {
                if (!resultMap.ContainsKey(fieldSchema))
                {
                    Logger<ConnectionCacheLoader>.WarnFormat(Resources.LogConCacheLdrFieldDefNotFound, fieldSchema.SchemaFullName, fieldSchema.SchemaMappedName);
                }
            }
            return resultMap;
        }

        private void LoadFieldDefMapBy(IDictionary<SchemaBase, FieldDefinition> fieldDefinitionMap, FieldDefinitionCollection defs, Func<FieldDefinition, SchemaBase> toMapField)
        {
            // initialize field definitions from TFS
            foreach (FieldDefinition def in defs)
            {
                SchemaBase toLoad = toMapField(def);

                if (toLoad != null)
                {
                    if (!fieldDefinitionMap.ContainsKey(toLoad))
                    {
                        fieldDefinitionMap[toLoad] = def;
                    }
                    else
                    {
                        Logger<ConnectionCacheLoader>.WarnFormat(Resources.LogConCacheLdrDuplicateFieldDef, toLoad.SchemaFullName, def.Name);
                    }
                }
            }
        }

        private CacheFieldDefinition LoadFieldDef(CacheContext dataContext, FieldDefinition def, SchemaBase fieldSchema, TfsWiTypeReader typeReader, CacheWorkflow workflow)
        {
            DataType fieldBackEndType = DataTypeConverter.ParseDataType(def.FieldType.ToString());
            Type fieldType = DataTypeConverter.ToClassType(fieldBackEndType);
            
            // special case: the field corresponds to the state field, the default
            //               value must be read out from the workflow.
            if (fieldSchema == workflow.FieldSchema)
            {
                return dataContext.ElementFactory.CreateFieldDefinition(
                    def.Id,
                    def.Name,
                    fieldSchema,
                    fieldBackEndType,
                    def.AllowedValues.Convert(aw => aw.ChangeType(fieldType)),
                    workflow.TransitionStart);
            }

            // default: create field definition as specified by the back-end system
            return dataContext.ElementFactory.CreateFieldDefinition(
                def.Id,
                def.Name,
                fieldSchema,
                fieldBackEndType,
                def.AllowedValues.Convert(aw => aw.ChangeType(fieldType)),
                typeReader.GetDefaultFieldValue(fieldSchema.SchemaMappedName, fieldType));
        }

        // PARTIAL CLASS:
        //  [Method] must be specified in a different file 
        partial void LoadMemberList(ref IList<CacheTeam> teamListToInit, ref IList<CacheMember> memberListToInit, TfsServerWrapper tfsInstance, CacheContext dataContext, Project project);

        #endregion

        #region Events
        //--------------------------------------------------------------------
        // Events
        //--------------------------------------------------------------------

        #endregion



        //--------------------------------------------------------------------
        // Nested Classes
        //--------------------------------------------------------------------

        private class WorkItemTypeInfo
        {
            #region Declarations
            //--------------------------------------------------------------------
            // Declarations
            //--------------------------------------------------------------------

            internal SchemaBase TypeSchema { get; set; }
            internal WorkItemType WiType { get; set; }

            #endregion

            #region Properties
            //--------------------------------------------------------------------
            // Properties
            //--------------------------------------------------------------------

            #endregion

            #region Constructors / Destructor
            //--------------------------------------------------------------------
            // Constructors / Destructor
            //--------------------------------------------------------------------

            #endregion

            #region Methods
            //--------------------------------------------------------------------
            // Methods
            //--------------------------------------------------------------------

            #endregion
        }
    }
}